package net.cyndeline.fov.mrpas.defaultImpl

import net.cyndeline.fov.mrpas.LineParser
import net.cyndeline.fov.mrpas.Quadrant
import net.cyndeline.fov.mrpas.Line
import com.escalatesoft.subcut.inject.BindingModule
import com.escalatesoft.subcut.inject.Injectable


class DefaultLineParser(implicit val bindingModule: BindingModule) extends LineParser with Injectable {
  
  def generateQuadrants(mapPosition: (Int, Int), map: Array[Array[Int]], radius: Int): (Quadrant, Quadrant, Quadrant, Quadrant) = {
    (parseUpperLeftQuadrant(mapPosition, map, radius: Int), 
        parseUpperRightQuadrant(mapPosition, map, radius: Int), 
        parseLowerLeftQuadrant(mapPosition, map, radius: Int), 
        parseLowerRightQuadrant(mapPosition, map, radius: Int))
  }
  
  def parseUpperLeftQuadrant(mapPosition: (Int, Int), map: Array[Array[Int]], radius: Int): Quadrant = {
    val lines = parseLines(mapPosition, map, radius, -1, -1)
    val quadrant = injectOptional[Quadrant] getOrElse new DefaultQuadrant()
    quadrant.linesFromVerticalBorder = lines._1
    quadrant.linesFromHorizontalBorder = lines._2
    quadrant
  }
  
  def parseUpperRightQuadrant(mapPosition: (Int, Int), map: Array[Array[Int]], radius: Int): Quadrant = {
    val lines = parseLines(mapPosition, map, radius, 1, -1)
    val quadrant = injectOptional[Quadrant] getOrElse new DefaultQuadrant()
    quadrant.linesFromVerticalBorder = lines._1
    quadrant.linesFromHorizontalBorder = lines._2
    quadrant
  }
  
  def parseLowerLeftQuadrant(mapPosition: (Int, Int), map: Array[Array[Int]], radius: Int): Quadrant = {
    val lines = parseLines(mapPosition, map, radius, -1, 1)
    val quadrant = injectOptional[Quadrant] getOrElse new DefaultQuadrant()
    quadrant.linesFromVerticalBorder = lines._1
    quadrant.linesFromHorizontalBorder = lines._2
    quadrant
  }
  
  def parseLowerRightQuadrant(mapPosition: (Int, Int), map: Array[Array[Int]], radius: Int): Quadrant = {
    val lines = parseLines(mapPosition, map, radius, 1, 1)
    val quadrant = injectOptional[Quadrant] getOrElse new DefaultQuadrant()
    quadrant.linesFromVerticalBorder = lines._1
    quadrant.linesFromHorizontalBorder = lines._2
    quadrant
  }
  
  /**
   * Parses a single quadrant into lines.
   * 
   * @param pos The position that field of view should be computed from.
   * 			Will not be included in any line.
   * @param map Integer matrix (first array is the x axis). 0 or less indicates
   * 			a tile that permits vision. 1 and above indicates an obstacle.
   * @param radius The number of tiles from the starting positions axis that
   * 			   a line should contain. If this exceeds the map size, the
   *       		   map size will be used instead.
   * @param dx How the lines should be parsed along the x axis (starting at
   * 		   the starting position). -1 indicates moving left, while 1 is 
   *      	   right.
   * @param dy How the lines should be parsed along the y axis (starting at
   * 		   the starting position). -1 indicates moving up, while 1 is 
   *      	   down.
   * @return Two lists of lines that starts from the orthogonal border of the
   * 		 starting position and ends at the diagonal of the quadrant. The
   *    	 first list contains lines from the vertical border, the second
   *      	 from the horizontal.
   */
  private def parseLines(pos: (Int, Int), map: Array[Array[Int]], radius: Int, dx: Int, dy: Int): (List[Line], List[Line]) = {
    if (pos._1 < 0 || pos._1 >= map.size || pos._2 < 0 || pos._2 >= map(0).size) {
      return (Nil, Nil)
    }
    
    /* Making sure the adjustment values are valid. */
    val xAdjust = if (dx < -1) { -1 } else if (dx > 1) { 1 } else if (dx == 0) { - 1 } else { dx }
    val yAdjust = if (dy < -1) { -1 } else if (dy > 1) { 1 } else if (dy == 0) { - 1 } else { dy }
    
    /* The stop for traversing an axis is either left/up or right/down of the
     * starting point, depending on which direction that is being traversed.
     */
    val stopX = if (xAdjust == -1) { Math.max(0, pos._1 - radius) }
    			else { Math.min(pos._1 + radius, map.length - 1) }
    
    val stopY = if (yAdjust == -1) { Math.max(0, pos._2 - radius) }
    			else { Math.min(pos._2 + radius, map(0).length - 1) }
    
    var verticalBorderLines = List[Line]()
    var horizontalBorderLines = List[Line]()
    
    /* This loop is repeated, once for each border orthogonal to its axis. 
     * This is due to x or y values being adjusted by one for the axis they're 
     * being added to, as well as the x and y axis switching places between
     * the loops.
     */
    
    /* Starts at one to compensate for the missing line that only contains
     * the starting position.
     */
    var processedLines = 1
    
    // Vertical border
    for (y_index <- (pos._2 + dy) to stopY by dy) {
      val line = injectOptional[Line] getOrElse new DefaultLine()
      
      for (x_index <- pos._1 to (pos._1 + (dx * processedLines)) by dx) {
        if ((dx == -1 && x_index >= stopX) || (dx == 1 && x_index <= stopX)) {
          line.addPosition((x_index, y_index))
          addObstacle(x_index, y_index, map, line)
        }
      }
      
      if (line.length > 0) { verticalBorderLines = verticalBorderLines :+ line }
      processedLines += 1
    }
    
    // Horizontal border
    processedLines = 1
    for (x_index <- (pos._1 + dx) to stopX by dx) {
      val line = injectOptional[Line] getOrElse new DefaultLine()
      
      for (y_index <- pos._2 to (pos._2 + (dy * processedLines)) by dy) {
        if ((dy == -1 && y_index >= stopY) || (dy == 1 && y_index <= stopY)) {
          line.addPosition((x_index, y_index))
          addObstacle(x_index, y_index, map, line)
        }
      }
      
      if (line.length > 0) { horizontalBorderLines = horizontalBorderLines :+ line }
      processedLines += 1
    }
    
    /* The final quadrant content. */
    (verticalBorderLines, horizontalBorderLines)
  }
  
  private def addObstacle(x: Int, y: Int, map: Array[Array[Int]], line: Line) {
    if (map(x)(y) > 0) {
      line.addObstacle((x, y))
    }
  }

}